package com.castle.fortress.admin.pay.service.impl;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.castle.fortress.admin.pay.dto.*;
import com.castle.fortress.admin.pay.enums.PayTypeEnum;
import com.castle.fortress.admin.pay.service.PayService;
import com.castle.fortress.common.entity.RespBody;
import com.castle.fortress.common.exception.BizException;
import com.castle.fortress.common.respcode.BizErrorCode;
import com.castle.fortress.common.utils.CommonUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;
import java.util.Date;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 微信支付
 * @@author castle
 */
public class WxPayClient implements PayService {

    private Verifier verifier;
    private PayWxConfigDto configDto;
    private CloseableHttpClient httpClient;
    //商户私钥
    private PrivateKey merchantPrivateKey;

    public WxPayClient(PayWxConfigDto configDto){
        this.configDto=configDto;
        try {
            if(StrUtil.isEmpty(this.configDto.getKeyPath())){
                throw new BizException(BizErrorCode.PAY_CONFIG_ERROR);
            }
            JSONArray a = new JSONArray(this.configDto.getKeyPath());
            JSONObject o = a.get(0,JSONObject.class);
            merchantPrivateKey = PemUtil
                    .loadPrivateKey(new URL(o.getStr("url")).openStream());
            // 获取证书管理器实例
            CertificatesManager certificatesManager = CertificatesManager.getInstance();
            // 向证书管理器增加需要自动更新平台证书的商户信息
            // 加载平台证书（mchId：商户号,mchSerialNo：商户证书序列号,apiV3Key：V3密钥）
            certificatesManager.putMerchant(this.configDto.getMchId(), new WechatPay2Credentials(configDto.getMchId(), new PrivateKeySigner(configDto.getMchSerialNo(), merchantPrivateKey)),configDto.getApiV3Key().getBytes(StandardCharsets.UTF_8));
            // 从证书管理器中获取verifier
            verifier = certificatesManager.getVerifier(this.configDto.getMchId());
        } catch (Exception e) {
            e.printStackTrace();
            throw new BizException(BizErrorCode.PAY_CONFIG_ERROR);
        }
        // 初始化httpClient
        this.httpClient = WechatPayHttpClientBuilder.create()
                .withMerchant(configDto.getMchId(), configDto.getMchSerialNo(), merchantPrivateKey)
                .withValidator(new WechatPay2Validator(verifier)).build();
    }

    @Override
    public RespBody<PayResponse> uniorder(PayRequest request) {
        ObjectMapper objectMapper = new ObjectMapper();
        ObjectNode rootNode = objectMapper.createObjectNode();
        rootNode.put("mchid",this.configDto.getMchId().trim())
                .put("description", StrUtil.isEmpty(request.getOrderName())?request.getOrderId():request.getOrderName())
                .put("notify_url", this.configDto.getNotifyUrl().trim())
                .put("out_trade_no", request.getOrderId().trim())
                .put("attach",this.configDto.getSceneCode().trim());
        //单位元转分
        rootNode.putObject("amount")
                .put("total", BigDecimal.valueOf(request.getOrderAmount()).movePointRight(2).intValue());
        String uri="https://api.mch.weixin.qq.com/v3/pay/transactions";
        if(PayTypeEnum.WXPAY_JSAPI.getCode().equals(this.configDto.getPayTypeCode())||PayTypeEnum.WXPAY_MINI.getCode().equals(this.configDto.getPayTypeCode())){
            uri+="/jsapi";
            rootNode.put("appid", this.configDto.getAppId());
            rootNode.putObject("payer")
                    .put("openid", request.getOpenId());
        }else if(PayTypeEnum.WXPAY_H5.getCode().equals(this.configDto.getPayTypeCode())){
            uri+="/h5";
            rootNode.put("appid", this.configDto.getAppId());
            ObjectNode sceneInfo = objectMapper.createObjectNode();
            sceneInfo.put("payer_client_ip", request.getPayerClientIp());
            sceneInfo.putObject("h5_info").put("type",request.getH5InfoType());
            rootNode.putPOJO("scene_info",sceneInfo);
        }else if(PayTypeEnum.WXPAY_NATIVE.getCode().equals(this.configDto.getPayTypeCode())){
            uri+="/native";
            rootNode.put("appid", this.configDto.getAppId().trim());
        }else if(PayTypeEnum.WXPAY_APP.getCode().equals(this.configDto.getPayTypeCode())){
            uri+="/app";
            rootNode.put("appid", this.configDto.getAppId());
        }else{
            throw new BizException(BizErrorCode.PAY_TYPE_ERROR);
        }
        HttpPost httpPost = new HttpPost(uri);
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Content-type","application/json; charset=utf-8");
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        CloseableHttpResponse response=null;
        String bodyAsString="";
        try {
            objectMapper.writeValue(bos, rootNode);
            httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
            response = httpClient.execute(httpPost);
            if(response==null || 200 != response.getStatusLine().getStatusCode()){
                throw new BizException(BizErrorCode.PAY_UNIORDER_WX_ERROR,response.getStatusLine().getStatusCode()+"");
            }
            bodyAsString = EntityUtils.toString(response.getEntity());
            System.out.println(bodyAsString);
        } catch (IOException e) {
            e.printStackTrace();
            throw new BizException(BizErrorCode.PAY_UNIORDER_WX_ERROR);
        }finally {
            if(response!=null){
                try {
                    response.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return RespBody.data(buildPayResponse(request,bodyAsString));
    }


    @Override
    public RespBody<RefundResponse> refund(RefundRequest refundRequest) {
        ObjectMapper objectMapper = new ObjectMapper();
        ObjectNode rootNode = objectMapper.createObjectNode();
        rootNode.put("out_trade_no", refundRequest.getOrderId())
                .put("out_refund_no", refundRequest.getRefundNo());
        //单位元转分
        rootNode.putObject("amount")
                .put("total", BigDecimal.valueOf(refundRequest.getOrderAmount()).movePointRight(2).intValue())
        .put("currency","CNY")
        .put("refund",BigDecimal.valueOf(refundRequest.getRefundAmount()).movePointRight(2).intValue());
        String uri="https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
        HttpPost httpPost = new HttpPost(uri);
        httpPost.addHeader("Accept", "application/json");
        httpPost.addHeader("Content-type","application/json; charset=utf-8");
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        CloseableHttpResponse response=null;
        String bodyAsString="";
        try {
            objectMapper.writeValue(bos, rootNode);
            httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
            response = httpClient.execute(httpPost);
            if(response==null || 200 != response.getStatusLine().getStatusCode()){
                throw new BizException(BizErrorCode.PAY_REFOUND_WX_ERROR,response.getStatusLine().getStatusCode()+"");
            }
            bodyAsString = EntityUtils.toString(response.getEntity());
            System.out.println(bodyAsString);
        } catch (IOException e) {
            e.printStackTrace();
            throw new BizException(BizErrorCode.PAY_REFOUND_WX_ERROR);
        }finally {
            if(response!=null){
                try {
                    response.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return RespBody.data(buildRefoundResponse(refundRequest,bodyAsString));
    }

    @Override
    public RespBody<PayResponse> asyncNotify(HttpServletRequest request, String notifyData, PayTypeEnum payType, String sceneCode) {
        PayResponse response=new PayResponse();
        //随机串
        String nonceStr = request.getHeader("Wechatpay-Nonce");
        //微信传递过来的签名
        String signature = request.getHeader("Wechatpay-Signature");
        //证书序列号（微信平台）
        String serialNo = request.getHeader("Wechatpay-Serial");
        //时间戳
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        System.out.println("nonceStr:"+nonceStr);
        System.out.println("signature:"+signature);
        System.out.println("serialNo:"+serialNo);
        System.out.println("timestamp:"+timestamp);
        //构造签名串
        //应答时间戳\n
        //应答随机串\n
        //应答报文主体\n
        String signStr = Stream.of(timestamp, nonceStr, notifyData).collect(Collectors.joining("\n", "", "\n"));
        System.out.println("signStr:"+signStr);
        try {
            if(verifiedSign(serialNo,signStr,signature)){
                //解析报文
                String result = decryptBody(notifyData);
                System.out.println("asyncNotify:"+result);
                if(StrUtil.isNotEmpty(result)){
                    JSONObject resultJson = new JSONObject(result);
                    response.setMchId(this.configDto.getMchId());
                    response.setAttach(this.configDto.getSceneCode());
                    //取用户的实际支付金额
                    response.setOrderAmount(BigDecimal.valueOf(resultJson.getJSONObject("amount").getInt("payer_total")).movePointLeft(2).doubleValue());
                    response.setOrderId(resultJson.getStr("out_trade_no"));
                    //微信支付订单号
                    response.setTradeNo(resultJson.getStr("transaction_id"));
                    response.setDateTime(resultJson.getDate("success_time"));
                    if(PayTypeEnum.WXPAY_JSAPI.getCode().equals(this.configDto.getPayTypeCode())||PayTypeEnum.WXPAY_MINI.getCode().equals(this.configDto.getPayTypeCode())){
                        response.setAppId(this.configDto.getAppId());
                    }else if(PayTypeEnum.WXPAY_H5.getCode().equals(this.configDto.getPayTypeCode())){
                        response.setAppId(this.configDto.getAppId());
                    }else if(PayTypeEnum.WXPAY_NATIVE.getCode().equals(this.configDto.getPayTypeCode())){
                        response.setAppId(this.configDto.getAppId());
                    }else if(PayTypeEnum.WXPAY_APP.getCode().equals(this.configDto.getPayTypeCode())){
                        response.setAppId(this.configDto.getAppId());
                    }

                }
            }else{
                throw new BizException(BizErrorCode.PAY_ASYNC_SIGN_VERIFY_FAIL);
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (GeneralSecurityException e) {
            e.printStackTrace();
        }
        return RespBody.data(response);
    }

    private PayResponse buildPayResponse(PayRequest request,String body){
        JSONObject bodyJson = new JSONObject(body);
        PayResponse response = new PayResponse();
        response.setMchId(this.configDto.getMchId());
        response.setAttach(request.getSceneCode());
        response.setOrderAmount(request.getOrderAmount());
        response.setOrderId(request.getOrderId());
        if(PayTypeEnum.WXPAY_JSAPI.getCode().equals(this.configDto.getPayTypeCode())||PayTypeEnum.WXPAY_MINI.getCode().equals(this.configDto.getPayTypeCode())){
            response.setAppId(this.configDto.getAppId());
            response.setTimeStamp(System.currentTimeMillis()+"");
            response.setNonceStr(System.currentTimeMillis()+"");
            response.setPrepayId(bodyJson.getStr("prepay_id"));
            response.setWxPackage("prepay_id="+response.getPrepayId());
            response.setSignType("RSA");
            try {
                response.setPaySign(sign(response.getTimeStamp(),response.getNonceStr(),response.getPrepayId()));
            } catch (Exception e) {
                e.printStackTrace();
                throw new BizException(BizErrorCode.PAY_UNIORDER_WX_ERROR);
            }
        }else if(PayTypeEnum.WXPAY_H5.getCode().equals(this.configDto.getPayTypeCode())){
            response.setAppId(this.configDto.getAppId());
            response.setH5Url(bodyJson.getStr("h5_url"));
        }else if(PayTypeEnum.WXPAY_NATIVE.getCode().equals(this.configDto.getPayTypeCode())){
            response.setAppId(this.configDto.getAppId());
            response.setCodeUrl(bodyJson.getStr("code_url"));
        }else if(PayTypeEnum.WXPAY_APP.getCode().equals(this.configDto.getPayTypeCode())){
            response.setAppId(this.configDto.getAppId());
            //对应拉起支付中的partnerid
            response.setMchId(this.configDto.getMchId());
            response.setPrepayId(bodyJson.getStr("prepay_id"));
            response.setTimeStamp(System.currentTimeMillis()+"");
            response.setNonceStr(System.currentTimeMillis()+"");
            response.setWxPackage("Sign=WXPay");
            try {
                response.setSign(signForApp(response.getTimeStamp(),response.getNonceStr(),response.getPrepayId()));
            } catch (Exception e) {
                e.printStackTrace();
                throw new BizException(BizErrorCode.PAY_UNIORDER_WX_ERROR);
            }
        }
        return response;

    }


    /**
     *
     * @param timeStamp
     * @param nonceStr
     * @param prepayId
     * @return
     * @throws InvalidKeyException
     * @throws NoSuchAlgorithmException
     * @throws SignatureException
     */
    public  String sign(String timeStamp,String nonceStr,String prepayId) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException {
        if(CommonUtil.verifyParamEmpty(timeStamp,nonceStr,prepayId)){
            throw new BizException(BizErrorCode.PAY_UNIORDER_WX_ERROR);
        }
        StringBuilder body = new StringBuilder(this.configDto.getAppId()+"\n").append(timeStamp+"\n").append(nonceStr+"\n").append("prepay_id="+prepayId+"\n");
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(merchantPrivateKey);
        sign.update(body.toString().getBytes());
        return Base64.getEncoder().encodeToString(sign.sign());
    }

    /**
     *
     * @param timeStamp
     * @param nonceStr
     * @param prepayId
     * @return
     * @throws InvalidKeyException
     * @throws NoSuchAlgorithmException
     * @throws SignatureException
     */
    public  String signForApp(String timeStamp,String nonceStr,String prepayId) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException {
        if(CommonUtil.verifyParamEmpty(timeStamp,nonceStr,prepayId)){
            throw new BizException(BizErrorCode.PAY_UNIORDER_WX_ERROR);
        }
        StringBuilder body = new StringBuilder(this.configDto.getAppId()+"\n").append(timeStamp+"\n").append(nonceStr+"\n").append(prepayId+"\n");
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(merchantPrivateKey);
        sign.update(body.toString().getBytes());
        return Base64.getEncoder().encodeToString(sign.sign());
    }

    private RefundResponse buildRefoundResponse(RefundRequest request,String body){
        JSONObject bodyJson = new JSONObject(body);
        RefundResponse response = new RefundResponse();
        response.setOrderId(bodyJson.getStr("out_trade_no"));
        response.setRefundNo(bodyJson.getStr("out_refund_no"));
        response.setTradeNo(bodyJson.getStr("refund_id"));
        response.setRefundFee(BigDecimal.valueOf(bodyJson.getJSONObject("amount").getInt("refund")).movePointLeft(2).toString());//分转元
        response.setOrderAmount(BigDecimal.valueOf(bodyJson.getJSONObject("amount").getInt("total")).movePointLeft(2).doubleValue());
        response.setRefundDate(StrUtil.isEmpty(bodyJson.getStr("create_time"))?new Date():DateUtil.parse(bodyJson.getStr("create_time"), DatePattern.UTC_SIMPLE_PATTERN));
        return response;
    }



    /**
     * 验证签名
     *
     * @param serialNo  微信平台-证书序列号
     * @param signStr   自己组装的签名串
     * @param signature 微信返回的签名
     * @return
     * @throws UnsupportedEncodingException
     */
    private boolean verifiedSign(String serialNo, String signStr, String signature) throws UnsupportedEncodingException {
        return verifier.verify(serialNo, signStr.getBytes("utf-8"), signature);
    }


    /**
     * 解密body的密文
     *
     * "resource": {
     *         "original_type": "transaction",
     *         "algorithm": "AEAD_AES_256_GCM",
     *         "ciphertext": "",
     *         "associated_data": "",
     *         "nonce": ""
     *     }
     *
     * @param body
     * @return
     */
    private String decryptBody(String body) throws UnsupportedEncodingException, GeneralSecurityException {

        AesUtil aesUtil = new AesUtil(configDto.getApiV3Key().getBytes(StandardCharsets.UTF_8));

        JSONObject object = new JSONObject(body);
        JSONObject resource = object.getJSONObject("resource");
        String ciphertext = resource.getStr("ciphertext");
        String associatedData = resource.getStr("associated_data");
        String nonce = resource.getStr("nonce");

        return aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);

    }

}